package com.atguigu.tingshu.order.adapter.impl;

import java.math.BigDecimal;
import java.util.List;

import com.atguigu.tingshu.account.client.UserAccountFeignClient;
import com.atguigu.tingshu.common.execption.GuiguException;
import com.atguigu.tingshu.common.rabbit.constant.MqConst;
import com.atguigu.tingshu.common.rabbit.service.RabbitService;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.common.result.ResultCodeEnum;
import com.atguigu.tingshu.model.order.LocalMsg;
import com.atguigu.tingshu.order.adapter.PayWay;
import com.atguigu.tingshu.order.mapper.LocalMsgMapper;
import com.atguigu.tingshu.order.service.OrderInfoService;
import com.atguigu.tingshu.vo.account.AccountLockResultVo;
import com.atguigu.tingshu.vo.account.AccountLockVo;
import com.atguigu.tingshu.vo.order.OrderInfoVo;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

/**
 * Author:huzhongkui
 * Date: 2025-03-28 星期五 10:48:19
 * Description 零钱支付逻辑
 */
@Service
public class RechargePayWayImpl implements PayWay {


    @Autowired
    private OrderInfoService orderInfoService;

    @Autowired
    private UserAccountFeignClient userAccountFeignClient;

    @Autowired
    private RabbitService rabbitService;

    @Autowired
    private LocalMsgMapper localMsgMapper;

    @Autowired
    private StringRedisTemplate redisTemplate;


    String unLockCacheKey = "";

    String minusCacheKey = "";

/*
本地消息表的设置只有两个：1.在order表中新增了一个表`t_local_msg`
2.在公共的model中新添加了一个对象，LocalMsg，里面有订单编号，以及状态，以及创建时间和更新时间。初始化时状态为0
本地消息表就是一个中间表，当账户微服务中的账户表解锁成功时，账户表发送rmq消息给订单微服务，订单微服务收到消息后，修改本地消息表，
修改状态为1。
 */
    /**
     * 定时任务  让发消息给解锁队列一直进行下去。只有等下游说我解锁成功了，定时任务就不发了。
     *每隔1小时发送一次。看谁失败了，谁失败了给谁发。
     * @param payWay
     * @return
     */
    @Scheduled(fixedDelay = 1000 * 60)
    public void scanLocalMsgAndRetryMsg() {//扫描本地消息表并重试


        // 1.查询本地消息表
        LambdaQueryWrapper<LocalMsg> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(LocalMsg::getStatus, 0);
        // 2.获取到消息没有修改的订单编号
        List<LocalMsg> localMsgs = localMsgMapper.selectList(wrapper);
        for (LocalMsg localMsg : localMsgs) {
            // 3.发送检索到消息状态是0的消息 选择发送
            String unLockStr = redisTemplate.opsForValue().get(unLockCacheKey);
            if (!StringUtils.isEmpty(unLockStr)) {
                rabbitService.sendMessage(MqConst.EXCHANGE_ACCOUNT, MqConst.ROUTING_ACCOUNT_UNLOCK, localMsg.getMsgContent());
            }
            String minusStr = redisTemplate.opsForValue().get(minusCacheKey);
            if (!StringUtils.isEmpty(minusStr)) {
                rabbitService.sendMessage(MqConst.EXCHANGE_ACCOUNT, MqConst.ROUTING_ACCOUNT_MINUS, localMsg.getMsgContent());
            }
        }

    }


    @Override
    public boolean isSupport(String payWay) {

        return "1103".equals(payWay); //判断成功说明是这个支付方式 1103
    }


    /**
     * 分布式事务的问题：
     * 1、rpc导致的分布式事务：利用异常机制反向修改（Seata）
     * 2、消息队列导致分布式事务。本地消息表+定时任务。
     * <p>
     * 1.使用OpenFeign的时候 由于重试机制，所以会导致接口被重复调用，因此一定要解决由于OpenFeign重试机制的幂等性
     * 2.使用消息队列的时候，由于为了保证消息百分百发送成功，也会引入重试机制（定时任务的重试） 那么也会导致下游消费者会重复消费消息，所以也一定要控制幂等性。
     * <p>
     * 分布式事务有且只有两种情况下会出现
     * 情况一：使用RPC远程调用会导致分布式事务：异常机制+反向操作【反向rpc 反向发消息】 使用Seata的分布式事务框架
     * 情况二：使用消息队列导致的分布式事务：本地消息表+重试机制（定时任务的重试）
     * seata只能解决rpc导致的分布式事务问题，不能解决消息队列的分布式事务（异步）问题。
     * @param orderInfoVo
     * @param userId
     * @param orderNo
     */

   //@GlobalTransactional
    @Override
    public void dealPayWay(OrderInfoVo orderInfoVo, Long userId, String orderNo) {
        // 零钱支付逻辑

        // 1.检查零钱是否充足（并且锁定余额）----------------------保证openFeign的幂等性
        // 如果零钱充足 才保存订单相关信息
        // 如果零钱不充足 不用保存订单相关信息
        // 远程调用账户微服务  只能用rpc 不能用消息队列
        AccountLockVo accountLockVo = prePareUserAccountVo(orderNo, userId, orderInfoVo);
        //远程调用账户微服务检查和锁定账户表，锁定金额+,可用余额-
        Result<AccountLockResultVo> lockResultVo = userAccountFeignClient.checkAndLockAmount(accountLockVo); // 锁余额的作用
        if (lockResultVo.getCode() != 200) {//  锁余额失败
            throw new GuiguException(ResultCodeEnum.ACCOUNT_LOCK_ERROR);
        }
        // 2.保存订单信息  -----------------------------------------保证消息队列的幂等性
        try {
            //在保存订单信息和发送rabbitmq消息之前先初始化本地消息表。这里就是防止，
            // 账户表扣减成功了，但是发送消息生成订单失败的问题。//另外防止扣减成功，生成订单失败时，反向修改账户表时在
            // 发送消息时再次失败的情况下，启动定时任务，一直发送消息。直到下游返回消息说反向修改成功才不让定时任务再执行。

            // 2.1 初始化本地消息表  162行//初始化本地消息表，避免出现，账户表扣减成功了，但是发送消息生成订单失败的问题。记录了订单号和它的初始状态为0
            initLocalMsg(orderNo); //本地消息表只存消息和消息的状态
            // 2.2 保存订单信息
            orderInfoService.saveOrderInfo(orderInfoVo, userId, orderNo);
            // 3.解锁以及真正的扣减余额（total_amount以及lock_amount） 为了保证远程的消息一定扣款成功，就需要定时任务。
            // rpc和发消息都可以
            rabbitService.sendMessage(MqConst.EXCHANGE_ACCOUNT, MqConst.ROUTING_ACCOUNT_MINUS, orderNo);

            // 4.处理【零钱】支付成功之后的事情
            orderInfoService.PaySuccess(userId, orderNo);
        } catch (GuiguException e) {//当保存订单信息失败时，需要还原锁定余额和可用余额
            // 反向修改账户微服务下的user_account库对应的表 lock_amount和avliable_amount)
            // PRC  OR  发消息 默认已经有rabbitmq的3次重试了，但是为了保证消息一定发送成功，使用了定时任务。
            rabbitService.sendMessage(MqConst.EXCHANGE_ACCOUNT, MqConst.ROUTING_ACCOUNT_UNLOCK, orderNo);//解锁
            unLockCacheKey = "unlock:fail:flag" + orderNo;
            redisTemplate.opsForValue().set(unLockCacheKey, "1");
         /*  // throw new GuiguException(201, "订单数据库的操作出现了异常");*/
        } catch (Exception e) { //当真正扣减余额失败时，需要修改 lock_amount(-)锁定余额 以及total_amount(-)总余额
            rabbitService.sendMessage(MqConst.EXCHANGE_ACCOUNT, MqConst.ROUTING_ACCOUNT_MINUS, orderNo);//消费
            minusCacheKey = "minus:fail:flag" + orderNo;
            redisTemplate.opsForValue().set(minusCacheKey, "1");
        }


    }
//初始化本地消息表，避免出现，账户表扣减成功了，但是发送消息生成订单失败的问题。记录了订单号和它的初始状态为0
    public void initLocalMsg(String orderNo) {
        LocalMsg localMsg = new LocalMsg();
        localMsg.setMsgContent(orderNo);
        localMsg.setStatus(0);
        localMsgMapper.insert(localMsg);
    }

    private AccountLockVo prePareUserAccountVo(String orderNo, Long userId, OrderInfoVo orderInfoVo) {

        AccountLockVo accountLockVo = new AccountLockVo();
        accountLockVo.setOrderNo(orderNo);
        accountLockVo.setUserId(userId);
        accountLockVo.setAmount(orderInfoVo.getOrderAmount());// 实际买商品要花的钱
        //get(0)：从该列表中获取第一个元素（索引为 0 的元素）。前面是一个list集合，get(0)是取出结合中的第一个元素。
//        get(0) 表示从订单详情列表中取出第一条详情记录，并将其商品名称设置为订单锁账户时的内容。这通常用于标识当前订单的主要商品或首个商品项。
        // 订单内容 即使是多个声音， 订单内容只取第一个声音名称，只是一个标识没实际意义，这个最后只是为了在流水表order_detail中有个名称。
        accountLockVo.setContent(orderInfoVo.getOrderDetailVoList().get(0).getItemName());

        return accountLockVo;

    }
}
